using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace HakoJS.SourceGenerator;

public partial class JSBindingGenerator
{
    private static void GenerateRegistry(SourceProductionContext context,
        (ImmutableArray<ObjectModel> Objects,
            ImmutableArray<ClassModel> Classes) data)
    {
        var sb = new StringBuilder();

        sb.AppendLine("// <auto-generated/>");
        sb.AppendLine("#nullable enable");
        sb.AppendLine();
        sb.AppendLine("using System;");
        sb.AppendLine("using HakoJS.Host;");
        sb.AppendLine("using HakoJS.VM;");
        sb.AppendLine("using HakoJS.SourceGeneration;");
        sb.AppendLine();
        sb.AppendLine("namespace HakoJS.Extensions;");
        sb.AppendLine();
        sb.AppendLine("/// <summary>");
        sb.AppendLine("/// Extension methods for registering generated type converters.");
        sb.AppendLine("/// </summary>");
        sb.AppendLine("internal static class GeneratedMarshalingExtensions");
        sb.AppendLine("{");

        // RegisterObjectConverters method
        GenerateRegisterMethod(sb, data.Objects);

        sb.AppendLine();

        // ProjectToJSValue method
        GenerateProjectorMethod(sb, data.Objects, data.Classes);

        sb.AppendLine("}");

        context.AddSource("GeneratedMarshalingExtensions.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
    }

    private static void GenerateRegisterMethod(StringBuilder sb, ImmutableArray<ObjectModel> objects)
    {
        sb.AppendLine("    /// <summary>");
        sb.AppendLine("    /// Registers all JSObject reifiers and the projector function defined in this assembly.");
        sb.AppendLine("    /// Call this once during application startup.");
        sb.AppendLine("    /// </summary>");
        sb.AppendLine("    /// <param name=\"runtime\">The HakoRuntime instance.</param>");
        sb.AppendLine("    public static void RegisterObjectConverters(this global::HakoJS.Host.HakoRuntime runtime)");
        sb.AppendLine("    {");

        if (objects.Length > 0)
        {
            sb.AppendLine("        // Register JSObject reifiers (JS → .NET)");
            foreach (var obj in objects)
            {
                var fullTypeName = string.IsNullOrEmpty(obj.SourceNamespace)
                    ? $"global::{obj.TypeName}"
                    : $"global::{obj.SourceNamespace}.{obj.TypeName}";

                sb.AppendLine(
                    $"        global::HakoJS.SourceGeneration.JSMarshalingRegistry.RegisterObjectReifier((uint){obj.GetTypeId()}, (realm, jsValue) => {fullTypeName}.FromJSValue(realm, jsValue));");
            }

            sb.AppendLine();
        }
        else
        {
            sb.AppendLine("        // No JSObject types to register");
            sb.AppendLine();
        }

        sb.AppendLine("        // Register projector function (.NET → JS)");
        sb.AppendLine(
            "        global::HakoJS.SourceGeneration.JSMarshalingRegistry.RegisterProjector(ProjectToJSValue);");

        sb.AppendLine("    }");
    }

    private static void GenerateProjectorMethod(StringBuilder sb,
        ImmutableArray<ObjectModel> objects,
        ImmutableArray<ClassModel> classes)
    {
        sb.AppendLine("    /// <summary>");
        sb.AppendLine("    /// Projects (converts) a .NET object to a JavaScript value.");
        sb.AppendLine("    /// </summary>");
        sb.AppendLine("    /// <param name=\"realm\">The realm to create the value in.</param>");
        sb.AppendLine("    /// <param name=\"obj\">The .NET object to convert.</param>");
        sb.AppendLine(
            "    /// <returns>A JSValue representing the object, or null if the type cannot be projected.</returns>");
        sb.AppendLine(
            "    private static global::HakoJS.VM.JSValue? ProjectToJSValue(global::HakoJS.VM.Realm realm, object obj)");
        sb.AppendLine("    {");
        sb.AppendLine("        return obj switch");
        sb.AppendLine("        {");

        // Collect all types and sort them by inheritance depth (derived types first)
        var allTypes = new List<(string FullTypeName, int Depth, INamedTypeSymbol? Symbol)>();

        // Add JSObjects
        foreach (var obj in objects)
        {
            var fullTypeName = string.IsNullOrEmpty(obj.SourceNamespace)
                ? $"global::{obj.TypeName}"
                : $"global::{obj.SourceNamespace}.{obj.TypeName}";
            var depth = GetInheritanceDepth(obj.TypeSymbol);
            allTypes.Add((fullTypeName, depth, obj.TypeSymbol));
        }

        // Add JSClasses
        foreach (var cls in classes)
        {
            var fullTypeName = string.IsNullOrEmpty(cls.SourceNamespace)
                ? $"global::{cls.ClassName}"
                : $"global::{cls.SourceNamespace}.{cls.ClassName}";
            var depth = GetInheritanceDepth(cls.TypeSymbol);
            allTypes.Add((fullTypeName, depth, cls.TypeSymbol));
        }

        // Sort by depth descending (derived types first), then by name for stability
        var sortedTypes = allTypes
            .OrderByDescending(t => t.Depth)
            .ThenBy(t => t.FullTypeName)
            .ToList();

        // Generate pattern matching cases
        foreach (var (fullTypeName, _, symbol) in sortedTypes)
        {
            var simpleName = symbol?.Name ?? ExtractSimpleTypeName(fullTypeName);
            var varName = EscapeIdentifierIfNeeded(char.ToLower(simpleName[0]) +
                                                   (simpleName.Length > 1 ? simpleName.Substring(1) : ""));

            // Ensure unique variable names
            if (varName == "obj") varName = "instance";

            sb.AppendLine($"            {fullTypeName} {varName} => {varName}.ToJSValue(realm),");
        }

        // Default case - return null
        sb.AppendLine("            _ => null");
        sb.AppendLine("        };");
        sb.AppendLine("    }");
    }

    private static int GetInheritanceDepth(INamedTypeSymbol? symbol)
    {
        if (symbol == null) return 0;

        var depth = 0;
        var current = symbol.BaseType;

        while (current != null && current.SpecialType != SpecialType.System_Object)
        {
            depth++;
            current = current.BaseType;
        }

        return depth;
    }
}